package com.atguigu.gulimall.product.service.impl;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.atguigu.gulimall.product.service.CategoryBrandRelationService;
import com.atguigu.gulimall.product.vo.Catelog2Vo;
import com.baomidou.mybatisplus.annotation.TableField;
import org.apache.commons.lang.StringUtils;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.Caching;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;

import java.lang.reflect.Array;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.atguigu.common.utils.PageUtils;
import com.atguigu.common.utils.Query;

import com.atguigu.gulimall.product.dao.CategoryDao;
import com.atguigu.gulimall.product.entity.CategoryEntity;
import com.atguigu.gulimall.product.service.CategoryService;
import org.springframework.transaction.annotation.Transactional;


@Service("categoryService")
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService {

//    @Autowired
//    private CategoryDao categoryDao;

    @Autowired
    private CategoryBrandRelationService categoryBrandRelationService;

    @Autowired
    StringRedisTemplate redisTemplate;

    @Autowired
    RedissonClient redissonClient;

    @Override
    public PageUtils queryPage(Map<String, Object> params) {
        IPage<CategoryEntity> page = this.page(
                new Query<CategoryEntity>().getPage(params),
                new QueryWrapper<CategoryEntity>()
        );

        return new PageUtils(page);
    }

    //查询：获取所有菜单信息，并组装成自关联的结构
    @Override
    public List<CategoryEntity> listWithTree() {

        //1.查出所有分类
        List<CategoryEntity> entities = baseMapper.selectList(null);

        //2.组装成父子的树状结构
        List<CategoryEntity> level1Menus = entities.stream()
                .filter(categoryEntity -> categoryEntity.getParentCid() == 0)  // ①filter过滤：找到所有的一级分类
                .map(menu -> {
                    menu.setChildren(getChildrens(menu, entities));
                    return menu;
                }) // ②map映射：每一个元素调用getChildrens()方法，找到自己的下级分类，并将下级分类保存到自己的属性Children中。
                .sorted((menu1, menu2) -> (menu1.getSort() == null ? 0 : menu1.getSort()) - (menu2.getSort() == null ? 0 : menu2.getSort())) //③sorted排序
                .collect(Collectors.toList()); //④collect结束操作：将流收集成为一个集合。

        return level1Menus;
    }

    //递归：从所有分类中，获取当前分类的所有子分类。
    private List<CategoryEntity> getChildrens(CategoryEntity root, List<CategoryEntity> all) {

        //注意：
        // 这个递归的结束条件没有明确写出。
        // 当当前root是三级分类时，经过filter方法过滤后，就不剩下任何元素了。（所有元素都被过滤了）
        // （map方法里不会出现空指针异常，可能是stream流有些特殊）
        // 此时就是递归的结束。
        // 即是 filter 过滤后，剩余元素为null时，就结束。（不清楚原理）

        List<CategoryEntity> children = all.stream().filter(
                categoryEntity -> categoryEntity.getParentCid().equals(root.getCatId())
        ).map(categoryEntity -> {
            //递归查找所有子分类的子分类
            categoryEntity.setChildren(getChildrens(categoryEntity, all));
            return categoryEntity;
        }).sorted((menu1, menu2) -> {
            return (menu1.getSort() == null ? 0 : menu1.getSort()) - (menu2.getSort() == null ? 0 : menu2.getSort());
        }).collect(Collectors.toList());
        return children;
    }

    //删除：根据catIds，批量删除Menus
    @Override
    public void removeMenusByIds(List<Long> asList) {
        //TODO 1.检查当前删除的菜单，是否被其他的地方引用
        baseMapper.deleteBatchIds(asList);
    }

    @Override
    public Long[] findCatelogPath(Long catelogId) {
        List<Long> paths = new ArrayList<>();
        //递归查询，获得完整 paths 路径
        paths = findPath(catelogId, paths);
        Collections.reverse(paths);
        return (Long[]) paths.toArray(new Long[paths.size()]);
    }

    /**
     * 级联更新关联的数据
     *
     * @param category
     * @CacheEvict: 触发将数据从缓存删除的操作。失效模式
     */
//    @Caching(evict={
//            @CacheEvict(value = "category", key = "'getLevelOneCategorys'"),
//            @CacheEvict(value = "category", key = "'getCatelogJson'")
//    })
    @CacheEvict(value = "category", allEntries = true)
    @Transactional
    @Override
    public void updateCascade(CategoryEntity category) {
        this.updateById(category);
        categoryBrandRelationService.updateCategory(category);
    }

    // 每一个需要缓存的数据，我们都来指定要放到哪个名字的缓存。【缓存的分区（按照业务类型分）】
    @Cacheable(value = {"category"}, key = "#root.method.name")
    //代表当前方法的返回值需要缓存，如果缓存中有，方法不用调用。如果缓存中没有，会调用该方法，最后将方法返回值存入缓存
    @Override
    public List<CategoryEntity> getLevelOneCategorys() {
        List<CategoryEntity> entities = this.list(new QueryWrapper<CategoryEntity>().eq("cat_level", 1));
        return entities;
    }

    @Cacheable(value = "category", key = "#root.methodName")
    @Override
    public Map<String, List<Catelog2Vo>> getCatelogJson() {
        //1、将数据库的多次查询变为一次，一次查询所有。
        List<CategoryEntity> categoryEntities1 = this.list(null);
        //1、查出所有1级分类
        List<CategoryEntity> levelOneCategorys = getParent_cid(categoryEntities1, 0L);
        //2、封装成一个 map
        Map<String, List<Catelog2Vo>> parent_cid = levelOneCategorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
            //查到当前1级分类的所有2级分类
            List<CategoryEntity> categoryEntities = getParent_cid(categoryEntities1, v.getCatId());
            //封装2级分类
            List<Catelog2Vo> collect = null;
            if (categoryEntities != null) {
                collect = categoryEntities.stream().map(item -> {
                    Catelog2Vo catelog2Vo = new Catelog2Vo(item.getParentCid().toString(), null, item.getCatId().toString(), item.getName());
                    //给2级分类查找并封装3级分类
                    List<CategoryEntity> catelog3 = getParent_cid(categoryEntities1, item.getCatId());
                    if (catelog3 != null && catelog3.size() > 0) {
                        List<Catelog2Vo.Catelog3Vo> collect1 = catelog3.stream().map(l -> {
                            Catelog2Vo.Catelog3Vo catelog3Vo = new Catelog2Vo.Catelog3Vo();
                            catelog3Vo.setCatalog2Id(l.getParentCid().toString());
                            catelog3Vo.setId(l.getCatId().toString());
                            catelog3Vo.setName(l.getName());
                            return catelog3Vo;
                        }).collect(Collectors.toList());
                        catelog2Vo.setCatalog3List(collect1);
                    }
                    return catelog2Vo;
                }).collect(Collectors.toList());
            }
            return collect;
        }));
        return parent_cid;
    }

    //TODO 产生堆外内存溢出：OutOfDirectMemoryError
    //springboot2.0以后默认使用lettuce作为操作redis的客户端，它使用netty进行网络通信
    //lettuce的bug导致netty堆外内存溢出。
    //解决方案：
    //①升级lettuce客户端 ②切换使用jedis
    public Map<String, List<Catelog2Vo>> getCatelogJson2() {
        //给缓存中存放json字符串，拿出的json字符串，还要逆转为能用的java对象 【序列化与反序列化】

        /**
         * 1、预防 缓存击穿、雪崩、穿透
         * ①空结果缓存，解决缓存穿透
         * ②缓存设置随机过期时间(加随机值)，解决缓存雪崩
         * ③加锁，解决缓存击穿
         */

        //1、加入缓存逻辑,缓存中存放的数据都是json字符串，JSON跨语言，跨平台兼容。
        String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
        if (StringUtils.isEmpty(catalogJSON)) {
            //2、如果缓存中没有数据，这查询数据库
            Map<String, List<Catelog2Vo>> catelogJsonFromdb = getCatelogJsonFromdbWithRedissonLock();
            return catelogJsonFromdb;
        }
        //3、如果缓存中有，则将取到的json转为我们指定的对象。
        Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catelog2Vo>>>() {
        });
        return result;
    }

    //TODO 缓存中的数据如何和数据库保持一致？
    //查询（最终使用的）：Redisson分布式锁:从DB查询并封装分类数据
    public Map<String, List<Catelog2Vo>> getCatelogJsonFromdbWithRedissonLock() {

        //1、获取锁，根据锁的名字来区分不同的锁。锁的粒度，越细越快
        RLock lock = redissonClient.getLock("CatelogJson-lock");
        lock.lock();//上锁
        Map<String, List<Catelog2Vo>> data = null;
        try {
            data = getDataFromDB();
        } finally {
            lock.unlock();//释放锁
        }
        return data;

    }

    //查询：本地锁:从DB查询并封装分类数据
    public Map<String, List<Catelog2Vo>> getCatelogJsonFromdbWithLocalLock() {
        /**
         *  锁对象：
         *  对于锁的调用者而言，只要是同一个对象，则能锁住所有线程。
         *  分析：
         *  1、synchronized (this)：
         *      this 代表当前实例，即是 CategoryServiceImpl 这个对象。对于SpringBoot而言，
         *      bean实例默认是单例的。整个容器中，只有一个 CategoryServiceImpl 对象。所以高并发时，
         *      所有线程都是调用的this,即同一个CategoryServiceImpl 实例，锁对象相同。
         *      this 能锁住所有线程。
         */
        //TODO synchronized(){}、lock()方法等，都是本地锁。没有考虑分布式集群情况下的问题。每一个 gulimall-product 服务都有一个自己的 this 实例。
        synchronized (this) {
            //双重锁逻辑
            //在得到锁，并即将查询DB之前，再访问一次缓存，如果任然没有数据，才去查询DB
            String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
            if (!StringUtils.isEmpty(catalogJSON)) {
                //如果，缓存中有数据，则直接取出并返回
                Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catelog2Vo>>>() {
                });
                return result;
            }
            return getDataFromDB();
        }
    }

    //查询：自制分布式锁:从DB查询并封装分类数据
    public Map<String, List<Catelog2Vo>> getCatelogJsonFromdbWithRedisLock() {

        //1、占分布式锁，去redis占领
        // setnx key value :不存在相应key时，才执行操作。注意，要加上过期时间。
        String uuid = UUID.randomUUID().toString();
        Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 300, TimeUnit.SECONDS);
        // 占领成功：返回true。反之false
        if (lock) {
            Map<String, List<Catelog2Vo>> data = null;
            try {
                //占领锁成功，即是加锁成功,执行操作
                data = getDataFromDB();
            } finally {
                //操作完毕后，需要释放分布式锁。即删除redis中的key。（必须先查询key，再删除）
                //原因：
                //  不能直接删除key，删除前还需要判断：查询当前redis中的key，还是不是自己加的key。
                //  如果当前程序由于卡顿等，已经执行了很长时间，超过了key设置的TTL过期时间，自己的key已经自动过期了。
                //  此时其他服务器就可以去redis中获取分布式锁，即现在的redis中的key，已经是其他服务加上的锁了。
                //  如果我们不判断就直接删除，会出现--->删除其他人的锁的问题。
                // 所以需要先 get 再 delete，所以需要执行get、delete这两条命令。
                // 两条命令需要整体执行，保证原子性，引入Lua脚本。（保证原子性）
                /**
                 *  Redis的lua脚步：
                 *      Redis中为什么引入Lua脚本？
                 *      Redis是高性能的key-value内存数据库，在部分场景下，是对关系数据库的良好补充。
                 *      Redis提供了非常丰富的指令集，官网上提供了200多个命令。但是某些特定领域，需要扩充若干指令原子性执行时，仅使用原生命令便无法完成。
                 *      Redis 为这样的用户场景提供了 lua 脚本支持，用户可以向服务器发送 lua 脚本来执行自定义动作，获取脚本的响应数据。
                 *    Redis 服务器会单线程原子性执行 lua 脚本，保证 lua 脚本在处理的过程中不会被任意其它请求打断。
                 */
                //先判断是不是自己的锁，是就删除。反之，不管。
                String script = "if redis.call('get',KEYS[1])==ARGV[1] then return redis.call('del',KEYS[1])else return 0 end";
                redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock", uuid));//给Lua脚本传递参数
            }
            return data;
        } else {
            //占领没成功，即是等待锁，重试
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {

            }
            return getCatelogJsonFromdbWithRedisLock();
        }
    }


    // 查询：从DB查询所有分类数据,并存入缓存
    public Map<String, List<Catelog2Vo>> getDataFromDB() {
        //双重逻辑锁，再次判断缓存中是否有数据
        String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
        if (!StringUtils.isEmpty(catalogJSON)) {
            Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catelog2Vo>>>() {
            });
            return result;
        }
        //1、将数据库的多次查询变为一次，一次查询所有。
        List<CategoryEntity> categoryEntities1 = this.list(null);
        //1、查出所有1级分类
        List<CategoryEntity> levelOneCategorys = getParent_cid(categoryEntities1, 0L);
        //2、封装成一个 map
        Map<String, List<Catelog2Vo>> parent_cid = levelOneCategorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
            //查到当前1级分类的所有2级分类
            List<CategoryEntity> categoryEntities = getParent_cid(categoryEntities1, v.getCatId());
            //封装2级分类
            List<Catelog2Vo> collect = null;
            if (categoryEntities != null) {
                collect = categoryEntities.stream().map(item -> {
                    Catelog2Vo catelog2Vo = new Catelog2Vo(item.getParentCid().toString(), null, item.getCatId().toString(), item.getName());
                    //给2级分类查找并封装3级分类
                    List<CategoryEntity> catelog3 = getParent_cid(categoryEntities1, item.getCatId());
                    if (catelog3 != null && catelog3.size() > 0) {
                        List<Catelog2Vo.Catelog3Vo> collect1 = catelog3.stream().map(l -> {
                            Catelog2Vo.Catelog3Vo catelog3Vo = new Catelog2Vo.Catelog3Vo();
                            catelog3Vo.setCatalog2Id(l.getParentCid().toString());
                            catelog3Vo.setId(l.getCatId().toString());
                            catelog3Vo.setName(l.getName());
                            return catelog3Vo;
                        }).collect(Collectors.toList());
                        catelog2Vo.setCatalog3List(collect1);
                    }
                    return catelog2Vo;
                }).collect(Collectors.toList());
            }
            return collect;
        }));
        //3、将查到的数据转换为json，并出入缓存
        String jsonString = JSON.toJSONString(parent_cid);
        redisTemplate.opsForValue().set("catalogJSON", jsonString, 1, TimeUnit.DAYS);
        return parent_cid;
    }

    private List<CategoryEntity> getParent_cid(List<CategoryEntity> selectList, Long parent_cid) {
        List<CategoryEntity> collect = selectList.stream().filter(item -> {
            return item.getParentCid() == parent_cid;
        }).collect(Collectors.toList());
        return collect;
    }

    /**
     * 递归方法，根据传入的catelogId，查询完整路径。即：父id + 子id 组成的数组。
     * [1,100,255]
     */
    public List<Long> findPath(Long catelogId, List<Long> paths) {
        //1.现将第一个 catelodId 放入 paths
        paths.add(catelogId);
        //2.根据catelogId获取该对象信息
        CategoryEntity entity = this.getById(catelogId);
        if (entity.getParentCid() != 0) {
            //3.如果有父id，则递归查询
            findPath(entity.getParentCid(), paths);
        }
        return paths;
    }
}